package sweetvsti;

import java.io.File;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import jvst.wrapper.VSTPluginAdapter;
import jvst.wrapper.valueobjects.MidiKeyName;
import jvst.wrapper.valueobjects.MidiProgramCategory;
import jvst.wrapper.valueobjects.MidiProgramName;
import jvst.wrapper.valueobjects.VSTEvent;
import jvst.wrapper.valueobjects.VSTEvents;
import jvst.wrapper.valueobjects.VSTMidiEvent;
import jvst.wrapper.valueobjects.VSTPinProperties;

public class SweetVSTi extends VSTPluginAdapter {
    // samples (must be power of 2 here)
    //private static final int sampleSize = 1048576;

    private int sampleSize = 0;
    private static final float MIDI_SCALER = 10.0f / 127.0f;
    private static final int NUM_PROGRAMS = 16;
    private static final int NUM_OUTPUTS = 2;
    private VSTiProgram[] programs = new VSTiProgram[NUM_PROGRAMS];
    private int currentProgram;
    private float freq;
    private float volume = 1.0f;
    private int channelPrograms[] = new int[NUM_PROGRAMS];
    private int currentNote = 60;
    private int currentVelocity;
    private int currentDelta;
    private boolean noteIsOn;
    // 再生中WAVEファイルの位置。sampleSizeで数えて何個目かを表す
    private double phase;
    private double basePhase = 1.0;
    // waveデータ
    private byte[] data = null;
    // 128鍵盤の周波数テーブル
    //private double freq_table[] = new double[0x7F];

    public SweetVSTi(long wrapper) {
        super(wrapper);
        log("Construktor SweetVSTi() START!");

        reset();

        log("Construktor SweetVSTi() INVOKED!");
    }

    public void reset() {

        Settings.instance().loadAudioFiles();

        //Util.makeFrequencyTable(this.freq_table);

        for (int i = 0; i < this.programs.length; i++) {
            this.programs[i] = new VSTiProgram();
        }
        for (int i = 0; i < this.channelPrograms.length; i++) {
            this.channelPrograms[i] = i;
        }

        this.setProgram(0);
        this.setNumInputs(0);// no input
        this.setNumOutputs(2);// 2 outputs, 1 for each oscillator
        this.canProcessReplacing(true);
        this.isSynth(true);
        this.setUniqueID(7842315);

        this.phase = 0.0;
        this.noteIsOn = false;
        this.currentDelta = 0;

        this.suspend();
    }

    @Override
    public void resume() {
        this.wantEvents(1); //deprecated as of vst2.4
        //keep it anyways to be backward compatible...
    }

    public void setSampleRate(float sampleRate) {
        this.basePhase = this.sampleSize / sampleRate;//format.getSampleRate();
    }

    public void setProgram(int index) {
        if (index < 0 || index >= NUM_PROGRAMS) {
            return;
        }

        VSTiProgram dp = this.programs[index];
        this.currentProgram = index;

        this.setParameter(VSTiProgram.PARAM_ID_VOLUME, dp.getVolume());
        this.setParameter(VSTiProgram.PARAM_ID_FREQ, dp.getFreq());
    }

    public void setProgramName(String name) {
        this.programs[this.currentProgram].setName(name);
    }

    public String getProgramName() {
        String name;

        if (this.programs[this.currentProgram].getName().equals("Init")) {
            name = this.programs[this.currentProgram].getName() + " " + (this.currentProgram + 1);
        } else {
            name = this.programs[this.currentProgram].getName();
        }

        return name;
    }

    public String getParameterLabel(int index) {
        String label = "";

        switch (index) {
            case VSTiProgram.PARAM_ID_FREQ:
                label = "Hz";
                break;
            case VSTiProgram.PARAM_ID_VOLUME:
                label = "dB";
                break;
        }

        return label;
    }

    public String getParameterDisplay(int index) {
        String text = "";

        switch (index) {
            case VSTiProgram.PARAM_ID_FREQ: {
                text = Float.toString(this.freq);
                break;
            }
            case VSTiProgram.PARAM_ID_VOLUME: {
                text = this.dbToString(this.volume);
                break;
            }
        }

        return text;
    }

    public String getParameterName(int index) {
        String label = "";

        switch (index) {
            case VSTiProgram.PARAM_ID_FREQ:
                label = "Freq 1";
                break;
            case VSTiProgram.PARAM_ID_VOLUME:
                label = "Volume";
                break;
        }

        return label;
    }

    public void setParameter(int index, float value) {
        VSTiProgram program = this.programs[this.currentProgram];

        switch (index) {
            case VSTiProgram.PARAM_ID_FREQ: {
                program.setFreq(value);
                this.freq = value;
                break;
            }
            case VSTiProgram.PARAM_ID_VOLUME: {
                program.setVolume(value);
                this.volume = value;
                break;
            }
        }

    }

    public float getParameter(int index) {
        float v = 0;

        switch (index) {
            case VSTiProgram.PARAM_ID_FREQ:
                v = this.freq;
                break;
            case VSTiProgram.PARAM_ID_VOLUME:
                v = this.volume;
                break;
        }
        return v;
    }

    @Override
    public VSTPinProperties getOutputProperties(int index) {
        VSTPinProperties ret = null;

        if (index < NUM_OUTPUTS) {
            ret = new VSTPinProperties();
            ret.setLabel("jVSTx " + (index + 1) + "d");
            ret.setFlags(VSTPinProperties.VST_PIN_IS_ACTIVE);
            if (index < 2) {
                // make channel 1+2 stereo
                ret.setFlags(ret.getFlags() | VSTPinProperties.VST_PIN_IS_STEREO);
            }
        }

        return ret;
    }

    public String getProgramNameIndexed(int category, int index) {
        String text = "";
        if (index < this.programs.length) {
            text = this.programs[index].getName();
        }
        if ("Init".equals(text)) {
            text = text + " " + index;
        }
        return text;
    }

    @Override
    public boolean copyProgram(int destination) {
        if (destination < NUM_PROGRAMS) {
            this.programs[destination] = this.programs[this.currentProgram];
            return true;
        }
        return false;
    }

    @Override
    public String getEffectName() {
        return "SweetVSTi";
    }

    public String getVendorString() {
        return "http://code.google.com/p/sweetjavavsti/";
    }

    public String getProductString() {
        return "SweetVSTi";
    }

    public int getNumPrograms() {
        return NUM_PROGRAMS;
    }

    public int getNumParams() {
        return VSTiProgram.NUM_PARAMS;
    }

    public boolean setBypass(boolean value) {
        return false;
    }

    public int getProgram() {
        return this.currentProgram;
    }

    public int getPlugCategory() {
        return VSTPluginAdapter.PLUG_CATEG_SYNTH;
    }

    public int canDo(String feature) {
        int ret = SweetVSTi.CANDO_NO;

        if (SweetVSTi.CANDO_PLUG_RECEIVE_VST_EVENTS.equals(feature)) {
            ret = SweetVSTi.CANDO_YES;
        }
        if (SweetVSTi.CANDO_PLUG_RECEIVE_VST_MIDI_EVENT.equals(feature)) {
            ret = SweetVSTi.CANDO_YES;
        }
        if (SweetVSTi.CANDO_PLUG_MIDI_PROGRAM_NAMES.equals(feature)) {
            ret = SweetVSTi.CANDO_YES;
        }

        return ret;
    }

    public boolean string2Parameter(int index, String value) {
        boolean ret = false;

        try {
            if (value != null) {
                this.setParameter(index, Float.parseFloat(value));
            }
            ret = true;
        } catch (Exception e) {
            log(e.toString());
        }

        return ret;
    }

    // midi program names:
    // as an example, GM names are used here. in fact, VstXSynth doesn't even support
    // multi-timbral operation so it's really just for demonstration.
    // a 'real' instrument would have a number of voices which use the
    // programs[channelProgram[channel]] parameters when it receives
    // a note on message.
    @Override
    public int getMidiProgramName(int channel, MidiProgramName mpn) {
        int prg = mpn.getThisProgramIndex();

        if (prg < 0 || prg >= 128) {
            return 0;
        }
        this.fillProgram(channel, prg, mpn);
        if (channel == 9) {
            return 1;
        }

        return 128;
    }

    @Override
    public int getCurrentMidiProgram(int channel, MidiProgramName mpn) {
        if (channel < 0 || channel >= 16 || mpn == null) {
            return -1;
        }

        int prg = this.channelPrograms[channel];
        mpn.setThisProgramIndex(prg);
        fillProgram(channel, prg, mpn);

        return prg;
    }

    @Override
    public int getMidiProgramCategory(int channel, MidiProgramCategory cat) {
        cat.setParentCategoryIndex(-1);	// -1:no parent category
        cat.setFlags(0);			// reserved, none defined yet, zero.

        int category = cat.getThisCategoryIndex();
        if (channel == 9) {
            cat.setName("Drums");
            return 1;
        }
        if (category >= 0 && category < GMNames.NUM_GM_CATEGORIES) {
            cat.setName(GMNames.GM_CATEGORIES[category]);
        }

        return GMNames.NUM_GM_CATEGORIES;
    }

    @Override
    public boolean hasMidiProgramsChanged(int channel) {
        return false;
        //this.updateDisplay()
    }

    // struct will be filled with information for 'thisProgramIndex' and 'thisKeyNumber'
    // if keyName is "" the standard name of the key will be displayed.
    // if false is returned, no MidiKeyNames defined for 'thisProgramIndex'.
    public boolean getMidiKeyName(long channel, MidiKeyName key) {
        return false;
    }

    //DEPRECATED SINCE 2.4!
    //process is ACCUMULATING the calculated floats to the output
    //BUT STILL, leave it there for backward compatibility (some hosts only call this one
    //and are not aware of processReplacing...)
    @Override
    public void process(float[][] inputs, float[][] outputs, int sampleFrames) {
        // process () is required, and accumulating (out += h)
        // processReplacing () is optional, and in place (out = h). even though
        // processReplacing () is optional, it is very highly recommended to support it
    }

    //processReplacing is REPLACING the calculated floats to the output
    public void processReplacing(float[][] inputs, float[][] outputs, int sampleFrames) {
        // waveディレクトリが変更された
        if (Settings.instance().isIsWaveBaseDirChanged()) {
            // 先にnote off
            for (int i = 0; i < outputs[0].length; i++) {
                outputs[0][i] = 0;
                outputs[1][i] = 0;
            }
            this.reset();
            Settings.instance().setIsWaveBaseDirChanged(false);
        }

        if (this.noteIsOn) {
            float[] outL = outputs[0];
            float[] outR = outputs[1];
            float vol = 1.0f;//this.volume * this.currentVelocity * MIDI_SCALER;
            int start = 0;

            if (this.currentDelta > 0) {
                if (this.currentDelta >= sampleFrames) { //future
                    this.currentDelta -= sampleFrames;
                    return;
                }
                for (int i = 0; i < this.currentDelta; i++) { //zero delta frames
                    outL[i] = 0;
                    outR[i] = 0;
                }
                start = this.currentDelta;
                sampleFrames -= this.currentDelta;
                this.currentDelta = 0;
            }

            int nSampels = this.sampleSize / 2;

            AudioInputStream stream = null;
            int charactor = Settings.instance().getCharactor();
            int key = this.currentNote;

            //Util.openMessageDialog("vol:" + vol + " / nSampels" + nSampels);
            //Util.openMessageDialog("key:" + key);
            //Util.openMessageDialog("sampleFrames:" + sampleFrames);
            File[][] files = Settings.instance().getFiles();
            if (key < 0) {
                key = 0;
            }
            if (key > files.length) {
                key = files.length - 1;
            }
            File file = files[key][charactor];
            try {
                if (file.exists()) {
                    stream = AudioSystem.getAudioInputStream(file);
                
                    this.sampleSize = stream.available();

                    this.data = new byte[sampleSize];
                        
                    stream.read(this.data, 0, sampleSize);
//                    AudioFormat format = stream.getFormat();
//                    DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
//                    SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
//                    line.open(format);
//                    line.start();
//
//                    for (int readBytes = stream.read(this.data, 0, sampleSize);
//                            readBytes >= 0;
//                            readBytes = stream.read(this.data, 0, sampleSize));
//
//                    line.drain();
//                    line.close();
                    stream.close();
                } else {
                    Util.openMessageDialog("発音ファイルが存在しません");
                    return;
                }
            } catch (Exception ex) {
                if (stream != null) {
                    try {
                        stream.close();
                        stream = null;
                    } catch (Exception exx) {
                        Util.openMessageDialog("ファイル読み込みに失敗しました");
                        return;
                    }
                }
                Util.openMessageDialog("ファイル読み込みに失敗しました");
                return;
            }
            
            float to_float_with_vol = 1.0f / 32768.0f * vol;
            for (int i = start; i < outL.length; i++) {
                //  aliasing 問題とかは未対応
                int left_pos = (this.phase < nSampels) ? (int) (this.phase * 2) : 0;
                int right_pos = (this.phase + 1 < nSampels) ? (int) (this.phase + 1) * 2 : 0;

                int left_value = (this.data[left_pos + 1]) << 8 | (this.data[left_pos] & 0xFF);
                int right_value = (this.data[right_pos + 1]) << 8 | (this.data[right_pos] & 0xFF);
                float left = left_value * to_float_with_vol;
                float right = right_value * to_float_with_vol;
                left = Math.min(1.0f, Math.max(left, -1.0f));
                right = Math.min(1.0f, Math.max(right, -1.0f));
                outL[i] = left;
                outR[i] = right;
                this.phase += 2;
            }
        } else {
            //note off
            for (int i = 0; i < outputs[0].length; i++) {
                outputs[0][i] = 0;
                outputs[1][i] = 0;
            }
        }
    }

    @Override
    public int processEvents(VSTEvents ev) {
        for (int i = 0; i < ev.getNumEvents(); i++) {
            if (ev.getEvents()[i].getType() != VSTEvent.VST_EVENT_MIDI_TYPE) {
                continue;
            }

            VSTMidiEvent event = (VSTMidiEvent) ev.getEvents()[i];
            byte[] midiData = event.getData();
            int status = midiData[0] & 0xf0;// ignoring channel

            if (status == 0x90 || status == 0x80) {
                // we only look at notes
                int note = midiData[1] & 0x7f;
                int velocity = midiData[2] & 0x7f;
                if (status == 0x80) {
                    velocity = 0;	// note off by velocity 0
                }
                if (velocity == 0 && (note == currentNote)) {
                    this.noteOff();
                } else {
                    this.noteOn(note, velocity, event.getDeltaFrames());
                }
            } else if (status == 0xb0) {
                // all notes off
                if (midiData[1] == 0x7e || midiData[1] == 0x7b) {
                    this.noteOff();
                }
            }
        }

        return 1;	// want more
    }

    private void noteOff() {
        noteIsOn = false;
    }

    private void noteOn(int note, int velocity, int delta) {
        currentNote = note;
        currentVelocity = velocity;
        currentDelta = delta;
        noteIsOn = true;
        this.phase = 0.0;
    }

    private void fillProgram(int channel, int prg, MidiProgramName mpn) {
        if (channel == 9) {
            //drums
            mpn.setName("Standard");
            mpn.setMidiProgram((byte) 0);
            mpn.setParentCategoryIndex(0);
        } else {
            mpn.setName(GMNames.GM_NAMES[prg]);
            mpn.setMidiProgram((byte) prg);
            mpn.setParentCategoryIndex(-1);	// for now

            for (int i = 0; i < GMNames.NUM_GM_CATEGORIES; i++) {
                if (prg >= GMNames.GM_CATEGORIES_FIRST_INDICES[i]
                        && prg < GMNames.GM_CATEGORIES_FIRST_INDICES[i + 1]) {
                    mpn.setParentCategoryIndex(i);
                    break;
                }
            }
        }
    }
}
